package com.varian.service.exception;

import com.varian.tool.contant.ResultConstant;
import com.varian.tool.result.R;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;
import java.util.Map;

/**
 * @author ben
 * @since 2024/6/27
 */
public class VarianErrorAttributes implements ErrorAttributes, HandlerExceptionResolver {

    private static final String ERROR_INTERNAL_ATTRIBUTE = VarianErrorAttributes.class.getName() + ".ERROR";

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        return getErrorAttributes(webRequest);
    }

    private Map<String, Object> getErrorAttributes(WebRequest webRequest) {
        R<Void> result = new R<>();
        result.setCode(ResultConstant.FAIL_CODE);
        addStatus(result, webRequest);
        addErrorDetails(result, webRequest);
        return result.toMap();
    }

    private void addStatus(R<Void> result, RequestAttributes webRequest) {
        Integer status = getAttribute(webRequest, RequestDispatcher.ERROR_STATUS_CODE);
        if (status == null) {
            result.setStatus(ResultConstant.UNKNOWN_ERROR.status())
                    .setMessage(ResultConstant.UNKNOWN_ERROR.message());
        } else {
            result.setStatus(status);
        }
    }

    private void addErrorDetails(R<Void> result, WebRequest webRequest) {
        Throwable error = getError(webRequest);
        if (error != null) {
            while (error instanceof ServletException && error.getCause() != null) {
                error = error.getCause();
            }
            result.setMessage(error.getMessage());
        }
        addErrorMessage(result, webRequest, error);
    }

    private void addErrorMessage(R<Void> result, WebRequest webRequest, Throwable error) {
        BindingResult bindingResult = extractBindingResult(error);
        if (bindingResult != null) {
            addMessageAndErrorsFromBindingResult(result, bindingResult);
        } else {
            MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
            if (methodValidationResult != null) {
                addMessageAndErrorsFromMethodValidationResult(result, methodValidationResult);
            } else {
                addExceptionErrorMessage(result, webRequest, error);
            }
        }
    }

    private void addExceptionErrorMessage(R<Void> result, WebRequest webRequest, Throwable error) {
        result.setMessage(getMessage(webRequest, error));
    }

    protected String getMessage(WebRequest webRequest, Throwable error) {
        Object message = getAttribute(webRequest, RequestDispatcher.ERROR_MESSAGE);
        if (!ObjectUtils.isEmpty(message)) {
            return message.toString();
        }
        if (error != null && StringUtils.hasLength(error.getMessage())) {
            return error.getMessage();
        }
        return "No message available";
    }

    private MethodValidationResult extractMethodValidationResult(Throwable error) {
        if (error instanceof MethodValidationResult methodValidationResult) {
            return methodValidationResult;
        }
        return null;
    }

    private void addMessageAndErrorsFromMethodValidationResult(R<Void> result, MethodValidationResult methodValidationResult) {
        List<ObjectError> errors = methodValidationResult.getAllErrors()
                .stream()
                .filter(ObjectError.class::isInstance)
                .map(ObjectError.class::cast)
                .toList();
        addMessageAndErrorsForValidationFailure(result, "method='" + methodValidationResult.getMethod() + "'", errors);
    }

    private void addMessageAndErrorsFromBindingResult(R<Void> result, BindingResult bindingResult) {
        addMessageAndErrorsForValidationFailure(result, "object='" + bindingResult.getObjectName() + "'", bindingResult.getAllErrors());
    }

    private void addMessageAndErrorsForValidationFailure(R<Void> result, String validated, List<ObjectError> errors) {
        result.setMessage("Validation failed for " + validated + ". Error count: " + errors.size());
    }

    private BindingResult extractBindingResult(Throwable error) {
        if (error instanceof BindingResult bindingResult) {
            return bindingResult;
        }
        return null;
    }

    @Override
    public Throwable getError(WebRequest webRequest) {
        Throwable exception = getAttribute(webRequest, ERROR_INTERNAL_ATTRIBUTE);
        if (exception == null) {
            exception = getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION);
        }
        return exception;
    }

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        storeErrorAttributes(request, ex);
        return null;
    }

    private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
        request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
    }

    @SuppressWarnings("unchecked")
    private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
        return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
    }
}
